大家好!「省錢拍拍」App 經過第七天的形象改造,目前為止,它仍然是一個只能「看」的 App,使用者無法輸入自己的消費數據。
今天,我們將跨出從「靜態」到「互動」最關鍵的一步。我們將:
TextField
來接收使用者輸入。Form
和 TextFormField
來對輸入的資料進行組合與驗證。準備好讓我們的 App 真正「活」起來了嗎?讓我們開始吧!
要新增消費,我們首先需要一個入口。最符合直覺的設計,就是在主畫面的右下角放置一個懸浮按鈕 (FloatingActionButton
)。
lib/main.dart
,找到 HomePage
的 Scaffold
,在裡面加入 floatingActionButton
屬性。// lib/main.dart -> HomePage -> build
// ...
return Scaffold(
appBar: AppBar( /* ... */ ),
// 在 Scaffold 加入 FAB
floatingActionButton: FloatingActionButton(
onPressed: () {
// 點擊後要執行的導航動作
},
child: const Icon(Icons.add),
),
body: Column( /* ... */ ),
);
lib
資料夾上按右鍵,選擇 New File
,將其命名為 add_transaction_page.dart
。在檔案中,先放入一個最基本的頁面結構:
// lib/add_transaction_page.dart
import 'package:flutter/material.dart';
class AddTransactionPage extends StatelessWidget {
const AddTransactionPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('新增一筆消費'),
),
body: const Center(
child: Text('這裡是新增消費的表單'),
),
);
}
}
lib/main.dart
,我們使用 Navigator.push
這個方法來實現頁面跳轉。// lib/main.dart (記得在頂部 import 新檔案)
import 'package:snapsaver/add_transaction_page.dart'; // 根據你的專案結構調整路徑
// ...
// lib/main.dart -> HomePage -> floatingActionButton
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigator.push 會將一個新的 "Route" 推送到導航堆疊上
Navigator.push(
context,
// MaterialPageRoute 是一種標準的頁面切換動畫效果
MaterialPageRoute(builder: (context) => const AddTransactionPage()),
);
},
child: const Icon(Icons.add),
),
// ...
重啟你的 App,點擊右下角的 +
按鈕,你應該能成功跳轉到新的「新增消費」頁面了!Flutter 會自動在 AppBar 左側加上返回按鈕。
TextField
是最基礎的文字輸入框。但要有效地使用它,我們需要一個「控制器」——TextEditingController
,來讀取、監聽或修改輸入框內的文字。
為了管理 TextEditingController
的生命週期,我們需要將 AddTransactionPage
從 StatelessWidget
轉換為 StatefulWidget
。
// lib/add_transaction_page.dart
// 轉換為 StatefulWidget
class AddTransactionPage extends StatefulWidget {
const AddTransactionPage({super.key});
@override
State<AddTransactionPage> createState() => _AddTransactionPageState();
}
class _AddTransactionPageState extends State<AddTransactionPage> {
// 1. 宣告 Controller
final TextEditingController _titleController = TextEditingController();
// 2. 在 dispose 方法中釋放 Controller,防止內存洩漏
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('新增一筆消費'),
),
body: ListView( // 使用 ListView 讓內容超出時可以滾動
padding: const EdgeInsets.all(16.0),
children: [
TextField(
controller: _titleController, // 3. 綁定 Controller
decoration: const InputDecoration(
labelText: '品項名稱', // 標籤文字
hintText: '例如:早餐', // 提示文字
border: OutlineInputBorder(), // 加上外框線
),
),
],
),
);
}
}
當我們有多個輸入框時,逐一管理和驗證會變得很麻煩。Form
Widget 就是為此而生。它搭配 TextFormField
(一個內建驗證功能的 TextField
),可以輕鬆實現整份表單的統一驗證與提交。
GlobalKey<FormState>
來作為 Form
的唯一標識,以便我們在外部可以操作這個 Form
。Form
包裹住所有的 TextFormField
。TextFormField
: 將 TextField
替換為 TextFormField
,並為其提供 validator
函式。onPressed
中,使用 _formKey.currentState!.validate()
來觸發所有欄位的驗證。將以上概念整合起來,打造一個包含品項、金額,並具備驗證功能的完整表單。
// lib/add_transaction_page.dart (完整範例)
import 'package:flutter/material.dart';
class AddTransactionPage extends StatefulWidget {
const AddTransactionPage({super.key});
@override
State<AddTransactionPage> createState() => _AddTransactionPageState();
}
class _AddTransactionPageState extends State<AddTransactionPage> {
// 1. 建立 GlobalKey
final _formKey = GlobalKey<FormState>();
// 2. 建立各個欄位的 Controller
final _titleController = TextEditingController();
final _amountController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
void _submitForm() {
// 3. 觸發驗證
if (_formKey.currentState!.validate()) {
// 如果驗證通過
final title = _titleController.text;
final amount = double.tryParse(_amountController.text) ?? 0.0;
print('品項: $title, 金額: $amount');
// 在這裡,我們未來會將資料傳回主頁面
// 驗證通過後,返回上一頁
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('新增一筆消費'),
),
body: Form( // 4. 用 Form 包裹
key: _formKey, // 5. 綁定 Key
child: ListView(
padding: const EdgeInsets.all(16.0),
children: [
// 6. 將 TextField 改為 TextFormField
TextFormField(
controller: _titleController,
decoration: const InputDecoration(labelText: '品項名稱'),
// 7. 加入驗證邏輯
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入品項名稱';
}
return null; // 回傳 null 代表驗證通過
},
),
const SizedBox(height: 16),
TextFormField(
controller: _amountController,
decoration: const InputDecoration(labelText: '金額'),
keyboardType: TextInputType.number, // 設定鍵盤為數字鍵盤
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入金額';
}
if (double.tryParse(value) == null) {
return '請輸入有效的數字';
}
return null;
},
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _submitForm, // 8. 綁定提交函式
child: const Text('儲存'),
),
],
),
),
);
}
}
現在,當你留白或輸入無效數字並按下「儲存」時,TextFormField
下方就會自動顯示我們定義的錯誤訊息!
今天完成了從 0 到 1 的互動性突破!我們學會了:
Navigator.push
在頁面間導航。TextEditingController
來管理 TextField
的內容。Form
, TextFormField
, GlobalKey
來打造一個具備完整驗證邏輯的表單。按下儲存後,資料只是被印在終端機裡,主畫面的列表並沒有更新。明天,我們將著手處理 Flutter 開發中最核心、也最有趣的話題之一:狀態管理 (State Management)。我們將學習如何將新頁面的資料,安全地傳遞回主頁面,並真正地更新我們的列表 UI!